繼承上一篇 Never trust user input 的精神,有一種使用者也是操作迅速,彷彿在玩 OSU (如上圖,音樂遊戲),結果不小心按到「刪除」...,然後PM就跑來跟你說客戶覺得產品「很難用」...இдஇ
常見處理方式不外乎就是用 comfirm 來 double check,或是使用 prompt 要求使用者輸入特定文字,例如在 Github 上要刪除專案時,就會要求你輸入特定字串才能進一步刪除。
另一種就是能夠取消的 request,當使用者按下按鈕時,會告知處理中,並附上一顆取消按鈕來讓使用者取消,Google 雲端硬碟上傳時也很常見到。
不論是哪種作法,在設計規劃時,可以依照這個動作的嚴重性來搭配對應的處理方式,本篇要製作的就是最後提及的—可取消功能。
當使用者觸發事件時,會進入 pending 的狀態,並提供可以取消的動作進一步取消剛剛觸發的事件。
理想是:
使用 alert 當作範例,並包裝成如下,現在只要點擊 Button 就會跳出 alert,這是預期之內的動作。
function showAlert(msg) {
alert("Message:" + msg)
}
function Example() {
return (
<Button onClick={() => showAlert("I am alert!")}>
TRIGGER
</Button>
)
}
與前一篇的 useDebounce 雷同,會使用到 setTimeout,但差別在於我們要主動控制而不依賴其他 state 來觸發,因此期望回傳:
function useEventControl(cb, delay) {
// 接受一個cb: callback, delay: 延遲時間(ms)
return [startEvent, isPending, cancelEvent]
//分別是 觸發 / 執行狀態 / 取消
}
當然也會有 useEffect,使用者會藉由「操控」 isPending 來觸發執行,useEffect 則有 isPending 當作 deps,改變時也會進一步觸發裡面的內容:
const timeoutRef = useRef(null)
const [isPending, setIsPending] = useState(false)
//開始
const startEvent () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
setIsPending(true)
}
//取消
const cancelEvent = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
setIsPending(false)
}
useEffect(() => {
if (isPending) {
timeoutRef.current = setTimeout(() => {
cb()
setIsPending(false)
}, delay)
}
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [isPending])
最後調整一下,完整如下:
function useEventControl(cb, delay = 2000) {
const timeoutRef = useRef(null)
const [isPending, setIsPending] = useState(false)
const argsRef = useRef(null)
const startEvent = useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
argsRef.current = args
setIsPending(true)
}, [])
const cancelEvent = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
setIsPending(false)
}, [])
useEffect(() => {
if (isPending) {
timeoutRef.current = setTimeout(() => {
cb(...argsRef.current)
setIsPending(false)
}, delay)
}
return () => clearTimeout(timeoutRef.current)
}, [isPending])
return [startEvent, isPending, cancelEvent]
}
Param | Type | Description |
---|---|---|
cb |
function | callback,當 setTimeout 的 delay 時間到時會觸發的動作 |
delay |
number | 單位ms , 定義 setTimeout 要 delay 多久,default 為 2000ms |
這裡預設的delay較久,用意是給使用者時間來反應取消
Return | Type | Description |
---|---|---|
startEvent |
function | 來觸發event,其中傳入的參數會進一步傳入一開始的cb |
isPending |
boolean | 呈現 setTimeout 是否執行的狀態 |
cancelEvent |
function | 取消evnet |
這邊我自己覺得寫起來「比較卡」的地方是要能夠讓 startEvent
接受參數傳入並再丟給 cb
使用,如果有好想法歡迎再跟我分享(鞭策) (๑•́ ₃ •̀๑)
function Example() {
const [startEvent, isPending, cancelEvent] = useEventControl(showAlert, 2000)
return (
<Stack>
<Button onClick={() => startEvent("YO")} isLoading={isPending}>
TRIGGER
</Button>
<Button onClick={cancelEvent}>CANCEL</Button>
</Stack>
)
}
我們把原本的 showAlert 傳入 useEventControl,並設定 2000ms 的 delay,再把 hook 回傳的內容放到各自的位置上。
Chakra-UI 的 <Button />
,接受一個 isLoading 來呈現 UI 的狀態,我們也可以把 hook 回傳的 pending 一起傳入。
這樣一來就完成啦!
相比前一篇 useDebounce,本篇實作的對 event 操控性較高,當然應用的場景也有所不同。
功能延伸上,也可以進一步加入 onDone, onCancel 來讓這個 hook 應對更多情境與狀況。